#include "asycipc/linux_diagnostics.hpp"

#include <chrono>
#include <filesystem>
#include <stdexcept>
#include <vector>

#if defined(__linux__) || defined(__APPLE__)

#include <optional>
#include <system_error>
#include <unordered_map>

namespace asycipc::linux_diagnostics {

namespace {

namespace fs = std::filesystem;

struct SessionEntry {
  bool has_in = false;
  bool has_out = false;
  std::optional<fs::file_time_type> last_write;
};

using SessionMap = std::unordered_map<std::string, SessionEntry>;

SessionMap CollectSessionsInternal(const fs::path& base_dir) {
  SessionMap sessions;
  std::error_code ec;
  if (!fs::exists(base_dir, ec) || !fs::is_directory(base_dir, ec)) {
    return sessions;
  }

  fs::directory_iterator it(base_dir, ec);
  if (ec) {
    return sessions;
  }
  const fs::directory_iterator end;
  for (; it != end; it.increment(ec)) {
    if (ec) {
      ec.clear();
      continue;
    }
    std::error_code status_ec;
    const auto status = it->symlink_status(status_ec);
    if (status_ec || status.type() != fs::file_type::fifo) {
      continue;
    }
    const std::string name = it->path().filename().string();
    const std::string prefix = "session-";
    if (name.rfind(prefix, 0) != 0) {
      continue;
    }
    bool is_in = false;
    if (name.size() >= 3 && name.compare(name.size() - 3, 3, "-in") == 0) {
      is_in = true;
    } else if (name.size() >= 4 && name.compare(name.size() - 4, 4, "-out") == 0) {
      is_in = false;
    } else {
      continue;
    }

    const std::size_t suffix = is_in ? 3 : 4;
    if (name.size() <= prefix.size() + suffix) {
      continue;
    }

    const std::string session_id =
        name.substr(prefix.size(), name.size() - prefix.size() - suffix);
    auto& entry = sessions[session_id];
    if (is_in) {
      entry.has_in = true;
    } else {
      entry.has_out = true;
    }

    std::error_code time_ec;
    const auto write_time = fs::last_write_time(it->path(), time_ec);
    if (!time_ec && (!entry.last_write || write_time > *entry.last_write)) {
      entry.last_write = write_time;
    }
  }
  return sessions;
}

std::chrono::seconds ComputeAge(const std::optional<fs::file_time_type>& ts,
                                fs::file_time_type::clock::time_point now) {
  if (!ts.has_value()) {
    return std::chrono::seconds::max();
  }
  const auto last = *ts;
  if (now <= last) {
    return std::chrono::seconds(0);
  }
  return std::chrono::duration_cast<std::chrono::seconds>(now - last);
}

}  // namespace

FifoHealthStats InspectFifoHealth(const std::filesystem::path& base_dir,
                                  std::chrono::seconds stale_age) {
  FifoHealthStats stats{};
  const auto sessions = CollectSessionsInternal(base_dir);
  stats.total_sessions = sessions.size();
  const bool consider_age = stale_age.count() > 0;
  const auto now = std::filesystem::file_time_type::clock::now();

  for (const auto& pair : sessions) {
    const auto& entry = pair.second;
    const bool has_both = entry.has_in && entry.has_out;
    const auto age = ComputeAge(entry.last_write, now);
    const bool stale = (age == std::chrono::seconds::max()) ||
                       (consider_age && age >= stale_age);

    if (has_both && !stale) {
      stats.active_sessions++;
    }
    if (!has_both) {
      stats.incomplete_sessions++;
    }
    if (stale || !has_both) {
      stats.stale_sessions++;
    }
  }
  return stats;
}

std::vector<FifoSessionDetail> CollectFifoSessions(const std::filesystem::path& base_dir,
                                                   std::chrono::seconds stale_age) {
  std::vector<FifoSessionDetail> output;
  const auto sessions = CollectSessionsInternal(base_dir);
  const bool consider_age = stale_age.count() > 0;
  const auto now = std::filesystem::file_time_type::clock::now();
  output.reserve(sessions.size());

  for (const auto& pair : sessions) {
    FifoSessionDetail detail;
    detail.session_id = pair.first;
    detail.has_in = pair.second.has_in;
    detail.has_out = pair.second.has_out;
    detail.age = ComputeAge(pair.second.last_write, now);
    detail.is_incomplete = !(detail.has_in && detail.has_out);
    detail.is_stale = detail.is_incomplete || detail.age == std::chrono::seconds::max() ||
                      (consider_age && detail.age >= stale_age);
    output.push_back(detail);
  }

  return output;
}

std::size_t CleanupFifoSessions(const std::filesystem::path& base_dir,
                                std::chrono::seconds stale_age) {
  const auto sessions = CollectSessionsInternal(base_dir);
  if (sessions.empty()) {
    return 0;
  }

  const bool consider_age = stale_age.count() > 0;
  const auto now = std::filesystem::file_time_type::clock::now();
  std::size_t cleaned = 0;

  for (const auto& pair : sessions) {
    const auto& entry = pair.second;
    const bool has_both = entry.has_in && entry.has_out;
    const auto age = ComputeAge(entry.last_write, now);
    const bool stale = (age == std::chrono::seconds::max()) ||
                       (consider_age && age >= stale_age);
    const bool incomplete = !has_both;
    if (!incomplete && !stale) {
      continue;
    }

    const std::string session_prefix = "session-" + pair.first;
    const std::filesystem::path in_path = base_dir / (session_prefix + "-in");
    const std::filesystem::path out_path = base_dir / (session_prefix + "-out");

    std::error_code ec;
    std::filesystem::remove(in_path, ec);
    ec.clear();
    std::filesystem::remove(out_path, ec);
    ++cleaned;
  }

  return cleaned;
}

}  // namespace asycipc::linux_diagnostics

#else

namespace asycipc::linux_diagnostics {

FifoHealthStats InspectFifoHealth(const std::filesystem::path&,
                                  std::chrono::seconds) {
  throw std::runtime_error("linux_diagnostics not supported on this platform");
}

std::vector<FifoSessionDetail> CollectFifoSessions(const std::filesystem::path&,
                                                   std::chrono::seconds) {
  throw std::runtime_error("linux_diagnostics not supported on this platform");
}

std::size_t CleanupFifoSessions(const std::filesystem::path&,
                                std::chrono::seconds) {
  throw std::runtime_error("linux_diagnostics not supported on this platform");
}

}  // namespace asycipc::linux_diagnostics

#endif
